/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.engine.impl.agenda;

import com.je.bpm.core.model.*;
import com.je.bpm.core.model.event.BoundaryEvent;
import com.je.bpm.core.model.event.StartEvent;
import com.je.bpm.core.model.gateway.Gateway;
import com.je.bpm.core.model.process.AdhocSubProcess;
import com.je.bpm.core.model.process.SubProcess;
import com.je.bpm.core.model.task.KaiteLoopUserTask;
import com.je.bpm.engine.ActivitiException;
import com.je.bpm.engine.delegate.DelegateExecution;
import com.je.bpm.engine.delegate.DelegateHelper;
import com.je.bpm.engine.delegate.ExecutionListener;
import com.je.bpm.engine.delegate.Expression;
import com.je.bpm.engine.delegate.event.ActivitiEventType;
import com.je.bpm.engine.delegate.event.impl.ActivitiEventBuilder;
import com.je.bpm.engine.impl.Condition;
import com.je.bpm.engine.impl.bpmn.behavior.KaiteBaseUserTaskActivityBehavior;
import com.je.bpm.engine.impl.bpmn.behavior.MultiInstanceActivityBehavior;
import com.je.bpm.engine.impl.bpmn.helper.SkipExpressionUtil;
import com.je.bpm.engine.impl.cmd.SubmitTypeEnum;
import com.je.bpm.engine.impl.context.Context;
import com.je.bpm.engine.impl.el.UelExpressionCondition;
import com.je.bpm.engine.impl.interceptor.CommandContext;
import com.je.bpm.engine.impl.persistence.entity.ExecutionEntity;
import com.je.bpm.engine.impl.persistence.entity.ExecutionEntityManager;
import com.je.bpm.engine.impl.util.CollectionUtil;
import com.je.bpm.engine.upcoming.UpcomingCommentInfoDTO;
import com.je.bpm.runtime.shared.operator.AbstractOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Operation that leaves the {@link FlowElement} where the {@link ExecutionEntity} is currently at
 * and leaves it following the sequence flow.
 */
public class TakeOutgoingSequenceFlowsOperation extends AbstractOperation {

    private static final Logger logger = LoggerFactory.getLogger(TakeOutgoingSequenceFlowsOperation.class);

    protected boolean evaluateConditions;

    public TakeOutgoingSequenceFlowsOperation(CommandContext commandContext, ExecutionEntity executionEntity, boolean evaluateConditions) {
        super(commandContext, executionEntity);
        this.evaluateConditions = evaluateConditions;
    }

    @Override
    public void run() {
        FlowElement currentFlowElement = getCurrentFlowElement(execution);
        clearVariable(execution);
        UpcomingCommentInfoDTO upcomingCommentInfoDTO = UpcomingCommentInfoDTO.build(
                SubmitTypeEnum.OUTGOING, currentFlowElement.getId(), execution.getProcessInstanceId());
        commandContext.getProcessEngineConfiguration().getActivitiUpcomingRun().completeUpcoming(upcomingCommentInfoDTO);
        // Compensation check 是否补偿
        if ((currentFlowElement instanceof Activity) && (((Activity) currentFlowElement)).isForCompensation()) {
            /*
             * If the current flow element is part of a compensation, we don't always
             * want to follow the regular rules of leaving an activity.
             * More specifically, if there are no outgoing sequenceflow, we simply must stop
             * the execution there and don't go up in the scopes as we usually do
             * to find the outgoing sequenceflow
             */
            cleanupCompensation();
            return;
        }

        // When leaving the current activity, we need to delete any related execution (eg active boundary events)
        cleanupExecutions(currentFlowElement);
        if (currentFlowElement instanceof FlowNode) {
            handleFlowNode((FlowNode) currentFlowElement);
        } else if (currentFlowElement instanceof SequenceFlow) {
            handleSequenceFlow();
        }
    }

    public void clearVariable(DelegateExecution miExecution) {
        List<DelegateExecution> list = new ArrayList<>();
        list.add(miExecution);
        if (miExecution.getParent() != null) {
            list.add(miExecution.getParent());
            if (miExecution.getParent().getParent() != null) {
                list.add(miExecution.getParent().getParent());
            }
        }
        for (DelegateExecution execution : list) {
            if (execution == null) {
                continue;
            }
            execution.removeVariable(MultiInstanceActivityBehavior.NUMBER_OF_INSTANCES);
            execution.removeVariable(MultiInstanceActivityBehavior.NUMBER_OF_ACTIVE_INSTANCES);
            execution.removeVariable(MultiInstanceActivityBehavior.NUMBER_OF_ACTIVE_AMOUNT);
            execution.removeVariable(MultiInstanceActivityBehavior.NUMBER_OF_COMPLETED_INSTANCES);
            execution.removeVariable("taskConfigs");
            execution.removeVariable(MultiInstanceActivityBehavior.NUMBER_OF_PASS_INSTANCES);
            execution.removeVariable(MultiInstanceActivityBehavior.NUMBER_OF_VETO_INSTANCES);
            execution.removeVariable(MultiInstanceActivityBehavior.NUMBER_OF_ABSTAIN_INSTANCES);
            execution.removeVariable(MultiInstanceActivityBehavior.PASS_PRINCIPAL);
            execution.removeVariable(MultiInstanceActivityBehavior.PRINCIPAL_OPINION);
            execution.removeVariable(MultiInstanceActivityBehavior.CUSTOMER_MESSAGE);
            execution.removeVariable(MultiInstanceActivityBehavior.COUNTERSIGN_PASS_TYPE);
            execution.removeVariable(MultiInstanceActivityBehavior.COUNTERSIGN_VOTE_ALL);
            execution.removeVariable(MultiInstanceActivityBehavior.PROCESSING_INFO);
            execution.removeVariable(KaiteBaseUserTaskActivityBehavior.PREV_ASSIGNEE);
            execution.removeVariable(KaiteBaseUserTaskActivityBehavior.DIRECT_TASK_NAME);
            execution.removeVariable(KaiteBaseUserTaskActivityBehavior.DIRECT_TASK_TARGET);
            execution.removeVariable("loopCounter");
        }
    }

    protected void handleFlowNode(FlowNode flowNode) {

        //处理活动结束
        handleActivityEnd(flowNode);
        //子流程处理
        if (flowNode.getParentContainer() != null && flowNode.getParentContainer() instanceof AdhocSubProcess) {
            handleAdhocSubProcess(flowNode);
        } else {//当前流程处理
            leaveFlowNode(flowNode);
        }
    }

    protected void handleActivityEnd(FlowNode flowNode) {
        // a process instance execution can never leave a flow node, but it can pass here whilst cleaning up
        // hence the check for NOT being a process instance 是流程实例类型
        if (!execution.isProcessInstanceType()) {
            //获取执行侦听器
            if (CollectionUtil.isNotEmpty(flowNode.getExecutionListeners())) {
                executeExecutionListeners(flowNode, ExecutionListener.EVENTNAME_END);
            }
            //记录活动结束
            commandContext.getHistoryManager().recordActivityEnd(execution, null);
            //子流程
            if (!(execution.getCurrentFlowElement() instanceof SubProcess) && !(flowNode.getBehavior() instanceof MultiInstanceActivityBehavior)) {
                Context.getProcessEngineConfiguration().getEventDispatcher().dispatchEvent(
                        ActivitiEventBuilder.createActivityEvent(ActivitiEventType.ACTIVITY_COMPLETED,
                                flowNode.getId(),
                                flowNode.getName(),
                                execution.getId(),
                                execution.getProcessInstanceId(),
                                execution.getProcessDefinitionId(),
                                flowNode));
            }
        }
    }

    protected void leaveFlowNode(FlowNode flowNode) {
        logger.debug("Leaving flow node {} with id '{}' by following it's {} outgoing sequenceflow",
                flowNode.getClass(),
                flowNode.getId(),
                flowNode.getOutgoingFlows().size());
        Object outgoing = null;
        if (!(flowNode instanceof StartEvent)) {
            outgoing = execution.getTransientVariable("outgoing");
            execution.removeTransientVariable("outgoing");
        }
        // Get default sequence flow (if set)
        String defaultSequenceFlowId = null;
        if (flowNode instanceof Activity) {
            if (outgoing != null) {
                defaultSequenceFlowId = outgoing.toString();
            } else {
                defaultSequenceFlowId = ((Activity) flowNode).getDefaultFlow();
            }
        } else if (flowNode instanceof Gateway) {
            defaultSequenceFlowId = ((Gateway) flowNode).getDefaultFlow();
        }

        // Determine which sequence flows can be used for leaving  确定哪些序列流可用于离开
        List<SequenceFlow> outgoingSequenceFlows = new ArrayList();
        if (outgoing == null || StringUtils.isEmpty(outgoing.toString())) {
            for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {
                String skipExpressionString = sequenceFlow.getSkipExpression();
                if (!SkipExpressionUtil.isSkipExpressionEnabled(execution, skipExpressionString)) {
                    Map<String, Object> bean = (Map<String, Object>) commandContext.getAttribute(AbstractOperator.BEAN);
                    Boolean isPass = DelegateHelper.isBoolean(sequenceFlow.getConditionExpression(), bean);
                    if (!evaluateConditions || (evaluateConditions && isPass
                            && (defaultSequenceFlowId == null || !defaultSequenceFlowId.equals(sequenceFlow.getId())))) {
                        outgoingSequenceFlows.add(sequenceFlow);
                    }
                } else if (flowNode.getOutgoingFlows().size() == 1 || SkipExpressionUtil.shouldSkipFlowElement(commandContext, execution, skipExpressionString)) {
                    // The 'skip' for a sequence flow means that we skip the condition, not the sequence flow.
                    outgoingSequenceFlows.add(sequenceFlow);
                }
            }
        }

        // Check if there is a default sequence flow 检查是否有默认的顺序流
        if (outgoingSequenceFlows.size() == 0 && evaluateConditions) { // The elements that set this to false also have no support for default sequence flow
            if (defaultSequenceFlowId != null) {
                for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {
                    if (defaultSequenceFlowId.equals(sequenceFlow.getId())) {
                        outgoingSequenceFlows.add(sequenceFlow);
                        break;
                    }
                }
                if (outgoingSequenceFlows.size() == 0) {
                    if (flowNode instanceof KaiteLoopUserTask) {
                        for (SequenceFlow sequenceFlow : flowNode.getIncomingFlows()) {
                            if (defaultSequenceFlowId.equals(sequenceFlow.getId())) {
                                outgoingSequenceFlows.add(sequenceFlow);
                                break;
                            }
                        }
                    }
                }
            }
        }

        // No outgoing found. Ending the execution  未找到传出。 结束执行
        if (outgoingSequenceFlows.size() == 0) {
            if (flowNode.getOutgoingFlows() == null || flowNode.getOutgoingFlows().size() == 0) {
                logger.debug("No outgoing sequence flow found for flow node '{}'.", flowNode.getId());
                Context.getAgenda().planEndExecutionOperation(execution);
            } else {
                throw new ActivitiException("No outgoing sequence flow of element '" + flowNode.getId() + "' could be selected for continuing the process");
            }
        } else {
            // Leave, and reuse the incoming sequence flow, make executions for all the others (if applicable)
            ExecutionEntityManager executionEntityManager = commandContext.getExecutionEntityManager();
            List<ExecutionEntity> outgoingExecutions = new ArrayList(flowNode.getOutgoingFlows().size());
            SequenceFlow sequenceFlow = outgoingSequenceFlows.get(0);
            // Reuse existing one
            execution.setCurrentFlowElement(sequenceFlow);
            execution.setActive(true);
            outgoingExecutions.add(execution);

            // Executions for all the other one
            if (outgoingSequenceFlows.size() > 1) {
                for (int i = 1; i < outgoingSequenceFlows.size(); i++) {
                    ExecutionEntity parent = execution.getParentId() != null ? execution.getParent() : execution;
                    ExecutionEntity outgoingExecutionEntity = commandContext.getExecutionEntityManager().createChildExecution(parent);

                    SequenceFlow outgoingSequenceFlow = outgoingSequenceFlows.get(i);
                    outgoingExecutionEntity.setCurrentFlowElement(outgoingSequenceFlow);

                    executionEntityManager.insert(outgoingExecutionEntity);
                    outgoingExecutions.add(outgoingExecutionEntity);
                }
            }

            // Leave (only done when all executions have been made, since some queries depend on this)
            //离开（仅在所有执行完成后才完成，因为某些查询依赖于此）
            for (ExecutionEntity outgoingExecution : outgoingExecutions) {
                Context.getAgenda().planContinueProcessOperation(outgoingExecution);
            }
        }
    }

    protected void handleAdhocSubProcess(FlowNode flowNode) {
        boolean completeAdhocSubProcess = false;
        AdhocSubProcess adhocSubProcess = (AdhocSubProcess) flowNode.getParentContainer();
        if (adhocSubProcess.getCompletionCondition() != null) {
            Expression expression = Context.getProcessEngineConfiguration().getExpressionManager().createExpression(adhocSubProcess.getCompletionCondition());
            Condition condition = new UelExpressionCondition(expression);
            if (condition.evaluate(adhocSubProcess.getId(), execution)) {
                completeAdhocSubProcess = true;
            }
        }

        if (flowNode.getOutgoingFlows().size() > 0) {
            leaveFlowNode(flowNode);
        } else {
            commandContext.getExecutionEntityManager().deleteExecutionAndRelatedData(execution, null);
        }

        if (completeAdhocSubProcess) {
            boolean endAdhocSubProcess = true;
            if (!adhocSubProcess.isCancelRemainingInstances()) {
                List<ExecutionEntity> childExecutions = commandContext.getExecutionEntityManager().findChildExecutionsByParentExecutionId(execution.getParentId());
                for (ExecutionEntity executionEntity : childExecutions) {
                    if (!executionEntity.getId().equals(execution.getId())) {
                        endAdhocSubProcess = false;
                        break;
                    }
                }
            }

            if (endAdhocSubProcess) {
                Context.getAgenda().planEndExecutionOperation(execution.getParent());
            }
        }
    }

    protected void handleSequenceFlow() {
        commandContext.getHistoryManager().recordActivityEnd(execution, null);
        Context.getAgenda().planContinueProcessOperation(execution);
    }

    protected void cleanupCompensation() {
        // The compensation is at the end here. Simply stop the execution.
        commandContext.getHistoryManager().recordActivityEnd(execution, null);
        commandContext.getExecutionEntityManager().deleteExecutionAndRelatedData(execution, null);

        ExecutionEntity parentExecutionEntity = execution.getParent();
        if (parentExecutionEntity.isScope() && !parentExecutionEntity.isProcessInstanceType()) {
            if (allChildExecutionsEnded(parentExecutionEntity, null)) {
                // Go up the hierarchy to check if the next scope is ended too.
                // This could happen if only the compensation activity is still active, but the
                // main process is already finished.
                ExecutionEntity executionEntityToEnd = parentExecutionEntity;
                ExecutionEntity scopeExecutionEntity = findNextParentScopeExecutionWithAllEndedChildExecutions(parentExecutionEntity, parentExecutionEntity);
                while (scopeExecutionEntity != null) {
                    executionEntityToEnd = scopeExecutionEntity;
                    scopeExecutionEntity = findNextParentScopeExecutionWithAllEndedChildExecutions(scopeExecutionEntity, parentExecutionEntity);
                }
                if (executionEntityToEnd.isProcessInstanceType()) {
                    Context.getAgenda().planEndExecutionOperation(executionEntityToEnd);
                } else {
                    Context.getAgenda().planDestroyScopeOperation(executionEntityToEnd);
                }
            }
        }
    }

    protected void cleanupExecutions(FlowElement currentFlowElement) {
        if (execution.getParentId() != null && execution.isScope()) {
            // If the execution is a scope (and not a process instance), the scope must first be
            // destroyed before we can continue and follow the sequence flow
            Context.getAgenda().planDestroyScopeOperation(execution);
        } else if (currentFlowElement instanceof Activity) {
            // If the current activity is an activity, we need to remove any currently active boundary events
            Activity activity = (Activity) currentFlowElement;
            if (CollectionUtil.isNotEmpty(activity.getBoundaryEvents())) {
                // Cancel events are not removed
                List<String> notToDeleteEvents = new ArrayList<String>();
                for (BoundaryEvent event : activity.getBoundaryEvents()) {
                    if (CollectionUtil.isNotEmpty(event.getEventDefinitions()) &&
                            event.getEventDefinitions().get(0) instanceof CancelEventDefinition) {
                        notToDeleteEvents.add(event.getId());
                    }
                }
                // Delete all child executions
                Collection<ExecutionEntity> childExecutions = commandContext.getExecutionEntityManager().findChildExecutionsByParentExecutionId(execution.getId());
                for (ExecutionEntity childExecution : childExecutions) {
                    if (childExecution.getCurrentFlowElement() == null || !notToDeleteEvents.contains(childExecution.getCurrentFlowElement().getId())) {
                        commandContext.getExecutionEntityManager().deleteExecutionAndRelatedData(childExecution, null);
                    }
                }
            }
        }
    }

    // Compensation helper methods

    /**
     * @param executionEntityToIgnore The execution entity which we can ignore to be ended,
     *                                as it's the execution currently being handled in this operation.
     */
    protected ExecutionEntity findNextParentScopeExecutionWithAllEndedChildExecutions(ExecutionEntity executionEntity, ExecutionEntity executionEntityToIgnore) {
        if (executionEntity.getParentId() != null) {
            ExecutionEntity scopeExecutionEntity = executionEntity.getParent();
            // Find next scope
            while (!scopeExecutionEntity.isScope() || !scopeExecutionEntity.isProcessInstanceType()) {
                scopeExecutionEntity = scopeExecutionEntity.getParent();
            }
            // Return when all child executions for it are ended
            if (allChildExecutionsEnded(scopeExecutionEntity,
                    executionEntityToIgnore)) {
                return scopeExecutionEntity;
            }
        }
        return null;
    }

    protected boolean allChildExecutionsEnded(ExecutionEntity parentExecutionEntity, ExecutionEntity executionEntityToIgnore) {
        for (ExecutionEntity childExecutionEntity : parentExecutionEntity.getExecutions()) {
            if (executionEntityToIgnore == null || !executionEntityToIgnore.getId().equals(childExecutionEntity.getId())) {
                if (!childExecutionEntity.isEnded()) {
                    return false;
                }
                if (childExecutionEntity.getExecutions() != null && childExecutionEntity.getExecutions().size() > 0) {
                    if (!allChildExecutionsEnded(childExecutionEntity,
                            executionEntityToIgnore)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
}
